// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'package:file/file.dart';
import 'package:file/memory.dart';
import 'package:flutter_tools/src/base/file_system.dart';
import 'package:flutter_tools/src/base/logger.dart';
import 'package:flutter_tools/src/cache.dart';
import 'package:flutter_tools/src/commands/update_packages.dart';
import 'package:flutter_tools/src/dart/pub.dart';
import 'package:flutter_tools/src/project.dart';
import 'package:test/fake.dart';
import 'package:yaml/yaml.dart';

import '../../src/context.dart';
import '../../src/test_flutter_command_runner.dart';

// An example pubspec.yaml from flutter, not necessary for it to be up to date.
const String kFlutterPubspecYaml = r'''
name: flutter
description: A framework for writing Flutter applications
homepage: http://flutter.dev

environment:
  sdk: '>=3.2.0-0 <4.0.0'

dependencies:
  # To update these, use "flutter update-packages --force-upgrade".
  collection: 1.14.11
  meta: 1.1.8
  typed_data: 1.1.6
  vector_math: 2.0.8

  sky_engine:
    sdk: flutter

  gallery:
    git:
      url: https://github.com/flutter/gallery.git
      ref: d00362e6bdd0f9b30bba337c358b9e4a6e4ca950

dev_dependencies:
  flutter_test:
    sdk: flutter
  flutter_goldens:
    sdk: flutter

  archive: 2.0.11 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"

# PUBSPEC CHECKSUM: 1234
''';

// An example pubspec.yaml, not necessary for it to be up to date.
const String kExamplesPubspecYaml = r'''
name: examples
description: Examples for flutter
homepage: http://flutter.dev

version: 1.0.0

environment:
  sdk: '>=3.2.0-0 <4.0.0'
  flutter: ">=2.5.0-6.0.pre.30 <3.0.0"

dependencies:
  cupertino_icons: 1.0.4
  flutter:
    sdk: flutter

  archive: 2.0.11 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"

# PUBSPEC CHECKSUM: 6543
''';

const String kVersionJson = '''
{
  "frameworkVersion": "1.2.3",
  "channel": "[user-branch]",
  "repositoryUrl": "git@github.com:flutter/flutter.git",
  "frameworkRevision": "1234567812345678123456781234567812345678",
  "frameworkCommitDate": "2024-02-06 22:26:52 +0100",
  "engineRevision": "abcdef01abcdef01abcdef01abcdef01abcdef01",
  "dartSdkVersion": "1.2.3",
  "devToolsVersion": "1.2.3",
  "flutterVersion": "1.2.3"
}
''';

void main() {
  group('update-packages', () {
    late FileSystem fileSystem;
    late Directory flutterSdk;
    late Directory flutter;
    late FakePub pub;
    late FakeProcessManager processManager;
    late BufferLogger logger;

    setUpAll(() {
      Cache.disableLocking();
      logger = BufferLogger.test();
    });

    setUp(() {
      fileSystem = MemoryFileSystem.test();
      flutterSdk = fileSystem.directory('flutter')..createSync();
      flutterSdk.childFile('version').writeAsStringSync('1.2.3');
      flutterSdk.childDirectory('bin').childDirectory('cache').childFile('flutter.version.json')
        ..createSync(recursive: true)
        ..writeAsStringSync(kVersionJson);
      flutter = flutterSdk.childDirectory('packages').childDirectory('flutter')
        ..createSync(recursive: true);
      flutterSdk.childDirectory('dev').createSync(recursive: true);
      flutterSdk.childDirectory('examples').childFile('pubspec.yaml')
        ..createSync(recursive: true)
        ..writeAsStringSync(kExamplesPubspecYaml);
      flutter.childFile('pubspec.yaml').writeAsStringSync(kFlutterPubspecYaml);
      Cache.flutterRoot = flutterSdk.absolute.path;
      pub = FakePub(fileSystem);
      processManager = FakeProcessManager.empty();
    });

    testUsingContext('updates packages', () async {
      final UpdatePackagesCommand command = UpdatePackagesCommand();
      await createTestCommandRunner(command).run(<String>['update-packages']);
      expect(pub.pubGetDirectories, equals(<String>[
        '/.tmp_rand0/flutter_update_packages.rand0/synthetic_package',
        '/flutter/examples',
        '/flutter/packages/flutter',
      ]));
      expect(pub.pubBatchDirectories, isEmpty);
    }, overrides: <Type, Generator>{
      Pub: () => pub,
      FileSystem: () => fileSystem,
      ProcessManager: () => processManager,
      Cache: () => Cache.test(
        processManager: processManager,
      ),
    });

    testUsingContext('force updates packages', () async {
      final UpdatePackagesCommand command = UpdatePackagesCommand();
      await createTestCommandRunner(command).run(<String>[
        'update-packages',
        '--force-upgrade',
      ]);
      expect(pub.pubGetDirectories, equals(<String>[
        '/.tmp_rand0/flutter_update_packages.rand0/synthetic_package',
        '/flutter/examples',
        '/flutter/packages/flutter',
      ]));
      expect(pub.pubBatchDirectories, equals(<String>[
        '/.tmp_rand0/flutter_update_packages.rand0/synthetic_package',
      ]));
    }, overrides: <Type, Generator>{
      Pub: () => pub,
      FileSystem: () => fileSystem,
      ProcessManager: () => processManager,
      Cache: () => Cache.test(
        processManager: processManager,
      ),
    });

    testUsingContext('force updates packages --jobs=1', () async {
      final UpdatePackagesCommand command = UpdatePackagesCommand();
      await createTestCommandRunner(command).run(<String>[
        'update-packages',
        '--force-upgrade',
        '--jobs=1',
      ]);
      expect(pub.pubGetDirectories, equals(<String>[
        '/.tmp_rand0/flutter_update_packages.rand0/synthetic_package',
        '/flutter/examples',
        '/flutter/packages/flutter',
      ]));
      expect(pub.pubBatchDirectories, equals(<String>[
        '/.tmp_rand0/flutter_update_packages.rand0/synthetic_package',
      ]));
    }, overrides: <Type, Generator>{
      Pub: () => pub,
      FileSystem: () => fileSystem,
      ProcessManager: () => processManager,
      Cache: () => Cache.test(
        processManager: processManager,
      ),
    });

    testUsingContext('--transitive-closure --consumer-only', () async {
      final UpdatePackagesCommand command = UpdatePackagesCommand();
      await createTestCommandRunner(command).run(<String>[
        'update-packages',
        '--transitive-closure',
        '--consumer-only',
      ]);
      expect(pub.pubGetDirectories, equals(<String>[
        '/.tmp_rand0/flutter_update_packages.rand0/synthetic_package',
      ]));
      expect(pub.pubBatchDirectories, equals(<String>[
        '/.tmp_rand0/flutter_update_packages.rand0/synthetic_package',
      ]));
      // Expecting a line like:
      //   'flutter -> {collection, meta, typed_data, vector_math}'
      expect(
        logger.statusText,
        contains(RegExp(r'flutter -> {([a-z_]+, )*([a-z_]+)+}')),
      );
    }, overrides: <Type, Generator>{
      Pub: () => pub,
      FileSystem: () => fileSystem,
      ProcessManager: () => processManager,
      Cache: () => Cache.test(
        processManager: processManager,
      ),
      Logger: () => logger,
    });

    testUsingContext('--cherry-pick-package', () async {
      final UpdatePackagesCommand command = UpdatePackagesCommand();
      await createTestCommandRunner(command).run(<String>[
        'update-packages',
        '--cherry-pick-package=vector_math',
        '--cherry-pick-version=2.0.9',
      ]);
      expect(pub.pubGetDirectories, equals(<String>[
        '/.tmp_rand0/flutter_update_packages.rand0/synthetic_package',
        '/flutter/examples',
        '/flutter/packages/flutter',
      ]));
      expect(pub.pubBatchDirectories, equals(<String>[
        '/.tmp_rand0/flutter_update_packages.rand0/synthetic_package',
      ]));
      expect(pub.pubspecYamls, hasLength(3));
      final String output = pub.pubspecYamls.first;
      expect(output, isNotNull);
      expect(output, contains('collection: 1.14.11\n'));
      expect(output, contains('meta: 1.1.8\n'));
      expect(output, contains('typed_data: 1.1.6\n'));
      expect(output, contains('vector_math: 2.0.9\n'));
      expect(output, isNot(contains('vector_math: 2.0.8')));
      expect(output, isNot(contains('vector_math: ">= 2.0.8"')));
      expect(output, isNot(contains("vector_math: '>= 2.0.8'")));
    }, overrides: <Type, Generator>{
      Pub: () => pub,
      FileSystem: () => fileSystem,
      ProcessManager: () => processManager,
      Cache: () => Cache.test(
        processManager: processManager,
      ),
      Logger: () => logger,
    });

    testUsingContext('--force-upgrade', () async {
      final UpdatePackagesCommand command = UpdatePackagesCommand();
      await createTestCommandRunner(command).run(<String>[
        'update-packages',
        '--force-upgrade',
      ]);
      expect(pub.pubGetDirectories, equals(<String>[
        '/.tmp_rand0/flutter_update_packages.rand0/synthetic_package',
        '/flutter/examples',
        '/flutter/packages/flutter',
      ]));
      expect(pub.pubBatchDirectories, equals(<String>[
        '/.tmp_rand0/flutter_update_packages.rand0/synthetic_package',
      ]));
      expect(pub.pubspecYamls, hasLength(3));
      final String output = pub.pubspecYamls.first;
      expect(output, isNotNull);
      expect(output, contains("collection: '>= 1.14.11'\n"));
      expect(output, contains("meta: '>= 1.1.8'\n"));
      expect(output, contains("typed_data: '>= 1.1.6'\n"));
      expect(output, contains("vector_math: '>= 2.0.8'\n"));
      expect(output, isNot(contains('vector_math: 2.0.8')));
    }, overrides: <Type, Generator>{
      Pub: () => pub,
      FileSystem: () => fileSystem,
      ProcessManager: () => processManager,
      Cache: () => Cache.test(
        processManager: processManager,
      ),
      Logger: () => logger,
    });

    testUsingContext('force updates packages --synthetic-package-path', () async {
      final UpdatePackagesCommand command = UpdatePackagesCommand();
      const String dir = '/path/to/synthetic/package';
      await createTestCommandRunner(command).run(<String>[
        'update-packages',
        '--force-upgrade',
        '--synthetic-package-path=$dir',
      ]);
      expect(pub.pubGetDirectories, equals(<String>[
        '$dir/synthetic_package',
        '/flutter/examples',
        '/flutter/packages/flutter',
      ]));
      expect(pub.pubBatchDirectories, equals(<String>[
        '$dir/synthetic_package',
      ]));
    }, overrides: <Type, Generator>{
      Pub: () => pub,
      FileSystem: () => fileSystem,
      ProcessManager: () => processManager,
      Cache: () => Cache.test(
        processManager: processManager,
      ),
    });
  });

  group('generateFakePubspec', () {
    const String prevVersion = '1.2.0';
    testUsingContext('constrains package versions to >= previous version if doUpgrade: true', () {
      final String pubspecSource = generateFakePubspec(
        <PubspecDependency>[
          PubspecDependency(
            '  foo: $prevVersion',
            'foo',
            '',
            version: prevVersion,
            sourcePath: '/path/to/pubspec.yaml',
            kind: DependencyKind.normal,
            isTransitive: false,
            isDevDependency: false,
          ),
        ],
        doUpgrade: true,
      );
      final YamlMap pubspec = loadYaml(pubspecSource) as YamlMap;
      expect((pubspec['dependencies'] as YamlMap)['foo'], '>= $prevVersion');
    });

    testUsingContext('uses previous package versions doUpgrade: false', () {
      final String pubspecSource = generateFakePubspec(
        <PubspecDependency>[
          PubspecDependency(
            '  foo: $prevVersion',
            'foo',
            '',
            version: prevVersion,
            sourcePath: '/path/to/pubspec.yaml',
            kind: DependencyKind.normal,
            isTransitive: false,
            isDevDependency: false,
          ),
        ],
      );
      final YamlMap pubspec = loadYaml(pubspecSource) as YamlMap;
      expect((pubspec['dependencies'] as YamlMap)['foo'], prevVersion);
    });
  });
}

class FakePub extends Fake implements Pub {
  FakePub(this.fileSystem);

  final FileSystem fileSystem;
  final List<String> pubGetDirectories = <String>[];
  final List<String> pubBatchDirectories = <String>[];
  final List<String> pubspecYamls = <String>[];

  @override
  Future<void> get({
    required PubContext context,
    required FlutterProject project,
    bool upgrade = false,
    bool offline = false,
    bool generateSyntheticPackage = false,
    bool generateSyntheticPackageForExample = false,
    String? flutterRootOverride,
    bool checkUpToDate = false,
    bool shouldSkipThirdPartyGenerator = true,
    PubOutputMode outputMode = PubOutputMode.all,
  }) async {
    pubGetDirectories.add(project.directory.path);
    pubspecYamls.add(project.directory.childFile('pubspec.yaml').readAsStringSync());
    project.directory.childFile('pubspec.lock')
      ..createSync(recursive: true)
      ..writeAsStringSync('''
# Generated by pub
# See https://dart.dev/tools/pub/glossary#lockfile
packages:
  async:
    dependency: "direct dev"
    description:
      name: async
      url: "https://pub.dartlang.org"
    source: hosted
    version: "2.8.2"
sdks:
  dart: ">=2.14.0 <3.0.0"
''');
    fileSystem.currentDirectory
        .childDirectory('.dart_tool')
        .childFile('package_config.json')
      ..createSync(recursive: true)
      ..writeAsStringSync('{"configVersion":2,"packages":[]}');
  }

  @override
  Future<void> batch(
      List<String> arguments, {
        required PubContext context,
        String? directory,
        MessageFilter? filter,
        String failureMessage = 'pub failed',
      }) async {
    if (directory != null) {
      pubBatchDirectories.add(directory);
    }

'''
Dart SDK 2.16.0-144.0.dev
Flutter SDK 2.9.0-1.0.pre.263
flutter_api_samples 1.0.0

dependencies:
- cupertino_icons 1.0.4
- collection 1.15.0
- meta 1.7.0
- typed_data 1.3.0 [collection]
- vector_math 2.1.1

dev dependencies:

transitive dependencies:
- platform 3.1.0
- process 4.2.4 [file path platform]
'''.split('\n').forEach(filter!);
  }
}
